///////////////////////////////////////////////////////////////////////
//
// Experiment2:  more complicated version of Experiment1:
//
// - only adds synapses when there is not already a synapse
//   between the neurons
//
///////////////////////////////////////////////////////////////////////

import java.util.LinkedList ;
import java.util.Random ;
import java.awt.Color ;

public class Experiment2 extends ParameterFramework implements INeuralNetExperiment {
    private INeuron baseNeuron = null ;
    private ISynapse baseSynapse = null ;
    private INeuron [ ] n = null ;
    private int NUM_NEURONS = 40 ;
    private double alpha = 1 , k = .05 ;
    private Random r = new Random( ) ;
    private IAppliedCurrent Iapp ;
    private int runTime = 7000 , synapseGrowRate = 10000 ;
    private double gridSize = 40 , initialMeanV = .4 ;
    private int numBiased = 20 ;
    private double sizeNeuron = 1 ;
    private double initialSynPercent = 15 ;
    private double synapseVariance = .04 , neuronVariance = .3 ;
    private long randomSeed = -1 ;

    
    public Experiment2( IAppliedCurrent Iapp ) {
        init( Iapp ) ;
    }
    
    public Experiment2( ) {
        init( new ConstantCurrent( 0 ) ) ;
    }
    
    public INeuralNetExperiment clone( ) {
        Experiment2 c = new Experiment2( Iapp ) ;
        c.setParam( getParams( ) ) ;
        return c ;
    }
    
    
    private void init( IAppliedCurrent Iapp ) {
        String [ ] params = { "alpha" , "k" , "numNeurons" , "runTime" , "synapseGrowRate" ,
        "gridSize" , "initialMeanV" , "numBiased" , "sizeNeuron" ,
        "synapse variance" , "neuron variance" , "initial syn %" , "random seed"} ;
        this.Iapp = Iapp ;
        parameterNames = params ;
    }
    
    public String getName( ) {
        return "Experiment 2" ;
    }
    
    public String getParam( String name ) {
        if ( name.equals( "numNeurons" ) ) {
            return "" + NUM_NEURONS ;
        } else if ( name.equals( "sizeNeuron" ) ) {
            return "" + sizeNeuron ;
        } else if ( name.equals( "numBiased" ) ) {
            return "" + numBiased ;
        } else if ( name.equals( "runTime" ) ) {
            return "" + runTime ;
        } else if ( name.equals( "alpha" ) ) {
            return "" + alpha ;
        } else if ( name.equals( "k" ) ) {
            return "" + k ;
        } else if ( name.equals( "synapseGrowRate" ) ) {
            return "" + synapseGrowRate ;
        } else if ( name.equals( "gridSize" ) ) {
            return "" + gridSize ;
        } else if ( name.equals( "initialMeanV" ) ) {
            return "" + initialMeanV ;
        } else if ( name.equals( "initial syn %" ) ) {
            return "" + initialSynPercent ;
        } else if ( name.equals( "synapse variance" ) ) {
            return "" + synapseVariance ;
        } else if ( name.equals( "neuron variance" ) ) {
            return "" + neuronVariance ;
        } else if ( name.equals ( "random seed" ) ) {
            return "" + randomSeed ;
        } else {
            return null ;
        }
    }
    
    public void setParam( String name , String data ) {
        double value ;
        
        // all parameters are doubles or ints
        try {
            value = Double.parseDouble( data ) ;
        } catch ( NumberFormatException e ) {
            // not a double; ignore the set request
            return ;
        }
        if ( name.equals( "numNeurons" ) ) {
            NUM_NEURONS = ( int ) Math.round( value ) ;
        } else if ( name.equals( "sizeNeuron" ) ) {
            sizeNeuron = value ;
        } else if ( name.equals( "numBiased" ) ) {
            numBiased = ( int ) Math.round( value ) ;
        } else if ( name.equals( "runTime" ) ) {
            runTime = ( int ) Math.round( value ) ;
        } else if ( name.equals( "alpha" ) ) {
            alpha = value ;
        } else if ( name.equals( "k" ) ) {
            k = value ;
        } else if ( name.equals( "synapseGrowRate" ) ) {
            synapseGrowRate = ( int ) Math.round( value ) ;
        } else if ( name.equals( "gridSize" ) ) {
            gridSize = value ;
        } else if ( name.equals( "initialMeanV" ) ) {
            initialMeanV = value ;
        } else if ( name.equals( "synapse variance" ) ) {
            synapseVariance = value ;
        } else if ( name.equals( "neuron variance" ) ) {
            neuronVariance = value ;
        } else if ( name.equals( "initial syn %" ) ) {
            initialSynPercent = Math.min( Math.max( 0 , value ) , 100 ) ;
        } else if ( name.equals ( "random seed" ) ) {
            randomSeed = ( long ) Math.round ( value ) ;
        }
    }
    
    public LinkedList<INeuron> getNeurons( ) {
        LinkedList<INeuron> result = new LinkedList<INeuron> ( ) ;
        int i ;
        for ( i = 0 ; i < n.length ; i ++ ) {
            result.add( n [ i ] ) ;
        }
        return result ;
    }
    
    public void init( INeuron n , ISynapse s ) {
        baseNeuron = n ;
        baseSynapse = s ;
    }
    
    private boolean tryAddSynapse( INeuron n , INeuron m ) {
        double denom ;
        if ( n == m ) return false ;
        if ( m.hasSynapseFrom( n ) ) return false ;
        denom = Math.max( .000001 , m.dist( n ) - sizeNeuron ) ;
        if ( r.nextDouble( ) < k / denom ) {
            m.addSynapse( baseSynapse.clone( n , m , r.nextGaussian( ) * Math.sqrt( synapseVariance ) ) ) ;
            return true ;
        }
        return false ;
    }
    
    private void growSynapses( ) {
        int i , j ;
        int count = 0 ;
        for ( i = 0 ; i < NUM_NEURONS ; i ++ ) {
            for ( j = 0 ; j < NUM_NEURONS ; j ++ ) {
                if ( tryAddSynapse( n [ i ] , n [ j ] ) ) {
                    count ++ ;
                }
            }
        }
    }
    
    private void addNSynapses( int num ) {
        int count ;
        int i , j ;
        
        for ( count = 0 ; count < num ; ) {
            i = ( int ) ( NUM_NEURONS * r.nextDouble( ) ) ;
            j = ( int ) ( NUM_NEURONS * r.nextDouble( ) ) ;
            try {
                if ( tryAddSynapse( n [ i ] , n [ j ] ) ) {
                    count ++ ;
                }
            } catch ( Exception e ) {
                // shouldn't need this, but doesn't hurt anything
            }
        }
    }
    
    public void run( ) {
        int i , j ;
        
        if ( baseNeuron == null ) return ;
        
        if ( randomSeed == -1 ) {
            r = new Random ( ) ;
        } else {
            r = new Random ( randomSeed ) ;
        }
        
        // clone the neurons, position them, etc...
        n = new INeuron [ NUM_NEURONS ] ;
        for ( j = 0 ; j < NUM_NEURONS ; j ++ ) {
            n [ j ] = baseNeuron.clone( ) ;
            n[j].setTime( 0 ) ;
            n[j].setPotential( r.nextGaussian( ) * Math.sqrt( neuronVariance ) + initialMeanV ) ;
            n[j].setPlace( new Point( r.nextDouble( ) * gridSize , r.nextDouble( ) * gridSize ) ) ;
            if ( j < numBiased ) {
                n[j].setAppliedCurrent( Iapp ) ;
            }
        }
        
        System.out.println("Initializing...");
        // add initial synapses
        addNSynapses( ( int ) ( initialSynPercent * .01 * NUM_NEURONS * ( NUM_NEURONS - 1 ) ) ) ;
        
        System.out.println("Running...");
        // run
        for ( i = 0 ; i < runTime ; i ++ ) {
            if ( ( i % synapseGrowRate ) == 0 && i != 0 ) {
                growSynapses( ) ;
            }
            for ( j = 0 ; j < NUM_NEURONS ; j ++ ) {
                n[j].advance( ) ;
            }
            for ( j = 0 ; j < NUM_NEURONS ; j ++ ) {
                n[j].advanceSynapses( ) ;
            }
            System.out.println(" t=" + i);
        }
    }
    
    
    
    public void setCurrent( IAppliedCurrent Iapp ) {
        this.Iapp = Iapp ;
    }
    
    
    private Curve appliedCurrentCurve( int tStart , int tStop ) {
        int i ;
        OrderedPair [ ] pts = new OrderedPair [ tStop - tStart ] ;
        Curve result ;
        
        for ( i = 0 ; i < tStop - tStart ; i ++ ) {
            pts [ i ] = new OrderedPair( i + tStart , Iapp.amount( i + tStart ) ) ;
        }
        result = new Curve( pts , "Iapp" ) ;
        result.setColor( Color.BLUE ) ;
        return result ;
    }
    
    
    
    public LinkedList<Curve> getCurves( ) {
        return getCurves( n[0].getTime( ) - 500 , n[0].getTime( ) ) ;
    }
    
    
    public LinkedList<Curve> getCurves( int tStart , int tStop ) {
        LinkedList<Curve> result = new LinkedList<Curve> ( ) ;
        int j , t , t2 ;
        Curve c ;
        boolean hasFired ;
        
        if ( n == null ) return result ;
        
        // add a curve for the applied current
        result.add( appliedCurrentCurve( tStart , tStop ) ) ;
        
        
        // loop through the neurons, add each curve... but only
        // the ones that have fired an action potential in the interval in question
        t = n[0].getTime( ) ;
        for ( j = 0 ; j < NUM_NEURONS ; j ++ ) {
            hasFired = false ;
            for ( t2 = tStart ; t2 < tStop ; t2 ++ ) {
                if ( n[j].getPotential( t2 ) >= .5 ) {
                    hasFired = true ;
                    break ;
                }
            }
            if ( hasFired ) {
                c = n[j].getCurve( tStart , tStop ) ;
                c.id = "" + ( j + 1 ) ;
                result.add( c ) ;
            }
        }
        
        return result ;
    }
    
    
}
